package com.dbflow5.config;

import com.dbflow5.MapUtils;
import com.dbflow5.adapter.ModelAdapter;
import com.dbflow5.adapter.ModelViewAdapter;
import com.dbflow5.adapter.RetrievalAdapter;
import com.dbflow5.adapter.queriable.ListModelLoader;
import com.dbflow5.adapter.queriable.SingleModelLoader;
import com.dbflow5.adapter.saveable.ModelSaver;
import com.dbflow5.database.*;
import com.dbflow5.migration.Migration;
import com.dbflow5.observing.TableObserver;
import com.dbflow5.runtime.DirectModelNotifier;
import com.dbflow5.runtime.ModelNotifier;
import com.dbflow5.transaction.BaseTransactionManager;
import com.dbflow5.transaction.DefaultTransactionManager;
import com.dbflow5.transaction.ITransaction;
import com.dbflow5.transaction.Transaction;
import ohos.app.Context;

import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;

import com.dbflow5.annotation.WorkerThread;

/**
 * Description: The main interface that all Database implementations extend from. Use this to
 * pass in for operations and [Transaction].
 */
public abstract class DBFlowDatabase implements DatabaseWrapper {

    public DBFlowDatabase(){
        init();
    }

    enum JournalMode {
        Automatic,
        Truncate,

        WriteAheadLogging;

        public JournalMode adjustIfAutomatic(Context context) {
            if(this == Automatic){
                return this;
            }else {
                return WriteAheadLogging;
            }
        }
    }

    private final Map<Integer, List<Migration>> migrationMap = new HashMap<>();

    private final Map<Class<?>, ModelAdapter<?>> modelAdapterMap = new HashMap<>();

    private final Map<String, Class<?>> modelTableNames = new HashMap<>();

    private final Map<Class<?>, ModelViewAdapter<?>> modelViewAdapterMap = new LinkedHashMap<>();

    private final Map<Class<?>, RetrievalAdapter<?>> queryModelAdapterMap = new LinkedHashMap<>();

    /**
     * The helper that manages database changes and initialization
     */
    private OpenHelper _openHelper = null;

    /**
     * Allows for the app to listen for database changes.
     */
    private DatabaseCallback callback = null;

    /**
     * Used when resetting the DB
     */
    private boolean isResetting = false;

    public BaseTransactionManager transactionManager;

    DatabaseConfig databaseConfig = null;

    private ModelNotifier modelNotifier = null;

    private TableObserver tableObserver;

    /**
     * list
     *
     * @return a list of all model classes in this database.
     */
    public List<Class<?>> modelClasses() {
        return new ArrayList<>(modelAdapterMap.keySet());
    }

    /**
     * BaseModelView
     *
     * @return the [BaseModelView] list for this database.
     */
    public List<Class<?>> modelViews(){
        return new ArrayList<>(modelViewAdapterMap.keySet());
    }

    /**
     * modelViewAdapters
     *
     * @return The list of [ModelViewAdapter]. Internal method for
     * creating model views in the DB.
     */
    public List<ModelViewAdapter<?>> modelViewAdapters(){
        return new ArrayList<>(modelViewAdapterMap.values());
    }

    /**
     * RetrievalAdapter
     *
     * @return The list of [RetrievalAdapter]. Internal method for creating query models in the DB.
     */
    public List<RetrievalAdapter<?>> queryModelAdapters(){
        return new ArrayList<>(queryModelAdapterMap.values());
    }

    /**
     * getMigrations
     *
     * @return The map of migrations to DB version
     */
    public Map<Integer, List<Migration>> getMigrations(){
        return migrationMap;
    }


    /**
     * Returns true if the [openHelper] has been created.
     */
    private boolean isOpened = false;

    public boolean isOpened(){
        return isOpened;
    }

    private Lock closeLock;

    public Lock getCloseLock(){
        if(closeLock == null){
            closeLock = new ReentrantLock();
        }
        return closeLock;
    }

    private boolean writeAheadLoggingEnabled = false;

    //private OpenHelper openHelper;
    public OpenHelper openHelper(){
        OpenHelper helper = _openHelper;
        if (helper == null) {
            DatabaseConfig config = FlowManager.getConfig().databaseConfigMap.get(associatedDatabaseClassFile());
            if (config != null && config.openHelperCreator != null) {
                helper = config.openHelperCreator.createHelper(this, internalCallback);
            } else {
                helper = new OhosSQLiteOpenHelper(FlowManager.getContext(), this, internalCallback);
            }
            onOpenWithConfig(config, helper);
            _openHelper = helper;
        }
        return helper;
    }

    private void onOpenWithConfig(DatabaseConfig config, OpenHelper helper) {
        helper.performRestoreFromBackup();

        boolean wal = config != null && config.journalMode.adjustIfAutomatic(FlowManager.getContext()) == JournalMode.WriteAheadLogging;
        helper.setWriteAheadLoggingEnabled(wal);
        writeAheadLoggingEnabled = wal;
        isOpened = true;
    }

    public DatabaseWrapper getWritableDatabase(){
        return openHelper().database();
    }

    /**
     * getDatabaseName
     *
     * @return The name of this database as defined in [Database]
     */
    public String getDatabaseName(){
        String databaseName;
        if(databaseConfig == null || databaseConfig.databaseName == null){
            databaseName = associatedDatabaseClassFile().getSimpleName();
        }else {
            databaseName = databaseConfig.databaseName;
        }
        return databaseName;
    }

    /**
     * getDatabaseFileName
     *
     * @return The file name that this database points to
     */
    public String getDatabaseFileName(){
        return getDatabaseName() + getDatabaseExtensionName();
    }

    /**
     * getDatabaseExtensionName
     *
     * @return the extension for the file name.
     */
    public String getDatabaseExtensionName(){
        if(databaseConfig != null && databaseConfig.databaseExtensionName != null){
            return databaseConfig.databaseExtensionName;
        }
        return ".db";
    }

    /**
     * isInMemory
     *
     * @return True if the database will reside in memory.
     */
    public boolean isInMemory(){
        return databaseConfig.isInMemory;
    }

    /**
     * databaseVersion
     *
     * @return The version of the database currently.
     */
    public abstract int databaseVersion();

    /**
     * isForeignKeysSupported
     *
     * @return True if the [Database.foreignKeyConstraintsEnforced] annotation is true.
     */
    public abstract boolean isForeignKeysSupported();

    /**
     * associatedDatabaseClassFile
     *
     * @return The class that defines the [Database] annotation.
     */
    public abstract Class<?> associatedDatabaseClassFile();

    /**
     * isDatabaseIntegrityOk
     *
     * @return True if the database is ok. If backups are enabled, we restore from backup and will
     * override the return value if it replaces the main DB.
     */
    public boolean isDatabaseIntegrityOk(){
        return openHelper().isDatabaseIntegrityOk();
    }

    /**
     * Returns the associated table observer that tracks changes to tables during transactions on
     * the DB.
     *
     * @return TableObserver
     */
    public TableObserver tableObserver(){
        if(tableObserver == null) {
            // observe all tables
            List<Class<?>> classList = modelClasses();
            classList.addAll(modelViews());
            tableObserver = new TableObserver(this, classList);
        }
        return tableObserver;
    }

    private void init() {
        applyDatabaseConfig(FlowManager.getConfig().databaseConfigMap.get(associatedDatabaseClassFile()));
    }

    /**
     * Applies a database configuration object to this class.
     *
     * @param databaseConfig databaseConfig
     */
    private void applyDatabaseConfig(DatabaseConfig databaseConfig) {
        this.databaseConfig = databaseConfig;
        if (databaseConfig != null) {
            // initialize configuration if exists.
            Collection<TableConfig<?>> tableConfigCollection = databaseConfig.tableConfigMap.values();
            for (TableConfig<?> tableConfig : tableConfigCollection) {
                ModelAdapter<Object> modelAdapter = (ModelAdapter<Object>) modelAdapterMap.get(tableConfig.tableClass);
                if(modelAdapter == null){
                    continue;
                }

                if(tableConfig.listModelLoader != null) {
                    modelAdapter.setListModelLoader((ListModelLoader<Object>)tableConfig.listModelLoader);
                }
                if(tableConfig.singleModelLoader != null) {
                    modelAdapter.setSingleModelLoader((SingleModelLoader<Object>)tableConfig.singleModelLoader);
                }
                if(tableConfig.modelSaver != null) {
                    modelAdapter.setModelSaver((ModelSaver<Object>)tableConfig.modelSaver);
                }
            }
            callback = databaseConfig.callback;
        }

        if (databaseConfig == null || databaseConfig.transactionManagerCreator == null) {
            transactionManager = new DefaultTransactionManager(this);
        } else {
            transactionManager = databaseConfig.transactionManagerCreator.createManager(this);
        }
    }

    protected <T> void addModelAdapter(ModelAdapter<T> modelAdapter, DatabaseHolder holder) {
        holder.putDatabaseForTable(modelAdapter.table(), this);
        modelTableNames.put(modelAdapter.getName(), modelAdapter.table());
        modelAdapterMap.put(modelAdapter.table(), modelAdapter);
    }

    protected <T> void addModelViewAdapter(ModelViewAdapter<T> modelViewAdapter, DatabaseHolder holder) {
        holder.putDatabaseForTable(modelViewAdapter.table(), this);
        modelViewAdapterMap.put(modelViewAdapter.table(), modelViewAdapter);
    }

    protected <T> void addRetrievalAdapter(RetrievalAdapter<T> retrievalAdapter, DatabaseHolder holder) {
        holder.putDatabaseForTable(retrievalAdapter.table(), this);
        queryModelAdapterMap.put(retrievalAdapter.table(), retrievalAdapter);
    }

    protected void addMigration(int version, Migration migration) {
        List<Migration> list = MapUtils.getOrPut(migrationMap, version, new ArrayList<>());
        list.add(migration);
    }

    /**
     * Internal method used to create the database schema.
     *
     * @return List of Model Adapters
     */
    public List<ModelAdapter<?>> modelAdapters(){
        return new ArrayList<>(modelAdapterMap.values());
    }

    /**
     * Returns the associated [ModelAdapter] within this database for
     * the specified table. If the Model is missing the [Table] annotation,
     * this will return null.
     *
     * @param table The model that exists in this database.
     * @param <T> t
     * @return The ModelAdapter for the table.
     */
    <T> ModelAdapter<T> getModelAdapterForTable(Class<T> table) {
        return (ModelAdapter<T>)modelAdapterMap.get(table);
    }

    /**
     * The associated [ModelAdapter] within this database for the specified table name.
     *
     * @param tableName The name of the table in this db.
     * @return The associated [ModelAdapter] within this database for the specified table name.
     * If the Model is missing the [Table] annotation, this will return null.
     */
    Class<?> getModelClassForName(String tableName){
        return modelTableNames.get(tableName);
    }

    /**
     * getModelViewAdapterForTable
     *
     * @param table the VIEW class to retrieve the ModelViewAdapter from.
     * @return the associated [ModelViewAdapter] for the specified table.
     */
    <T> ModelViewAdapter<T> getModelViewAdapterForTable(Class<T> table){
        return (ModelViewAdapter<T>)modelViewAdapterMap.get(table);
    }

    /**
     * getQueryModelAdapterForQueryClass
     *
     * @param queryModel The [QueryModel] class
     * @param <T> t
     * @return The adapter that corresponds to the specified class.
     */
    <T> RetrievalAdapter<T> getQueryModelAdapterForQueryClass(Class<T> queryModel){
        return (RetrievalAdapter<T>)queryModelAdapterMap.get(queryModel);
    }

    ModelNotifier getModelNotifier() {
        ModelNotifier notifier = modelNotifier;
        if (notifier == null) {
            DatabaseConfig config = FlowManager.getConfig().databaseConfigMap.get(associatedDatabaseClassFile());
            if(config != null) {
                if (config.modelNotifier == null) {
                    notifier = DirectModelNotifier.get();
                } else {
                    notifier = config.modelNotifier;
                }
            }
        }
        modelNotifier = notifier;
        return notifier;
    }

    public <R> Transaction.Builder<R> beginTransactionAsync(ITransaction<R> transaction){
        return new Transaction.Builder<>(transaction, this);
    }

    public <R> Transaction.Builder<R> beginTransactionAsync(Function<DatabaseWrapper, R> transaction){
        return beginTransactionAsync((ITransaction<R>) transaction::apply);
    }

    /**
     * This should never get called on the main thread. Use [beginTransactionAsync] for an async-variant.
     * Runs a transaction in the current thread.
     *
     * @param transaction transaction
     * @param <R> r
     * @return result
     */
    @WorkerThread
    public <R> R executeTransaction(ITransaction<R> transaction) {
        try {
            beginTransaction();
            R result = transaction.execute(getWritableDatabase());
            setTransactionSuccessful();
            return result;
        } finally {
            endTransaction();
        }
    }

    /**
     * This should never get called on the main thread. Use [beginTransactionAsync] for an async-variant.
     * Runs a transaction in the current thread.
     *
     * @param transaction transaction
     * @param <R> r
     * @return executeTransaction
     */
    @WorkerThread
    public <R> R executeTransactionFunc(Function<DatabaseWrapper, R> transaction){
        return executeTransaction(transaction::apply);
    }

    /**
     * areConsistencyChecksEnabled
     *
     * @return True if the [Database.consistencyCheckEnabled] annotation is true.
     */
    public abstract boolean areConsistencyChecksEnabled();

    /**
     * backupEnabled
     *
     * @return True if the [Database.backupEnabled] annotation is true.
     */
    public abstract boolean backupEnabled();

    /**
     * Performs a full deletion of this database. Reopens the [OhosSQLiteOpenHelper] as well.
     *
     * Reapplies the [DatabaseConfig] if we have one.
     * @param databaseConfig sets a new [DatabaseConfig] on this class.
     */
    public void reset(DatabaseConfig databaseConfig) {
        if(databaseConfig == null){
            databaseConfig = this.databaseConfig;
        }
        if (!isResetting) {
            destroy();
            // reapply configuration before opening it.
            applyDatabaseConfig(databaseConfig);
            openHelper().database();
        }
    }

    /**
     * Reopens the DB with the new [DatabaseConfig] specified.
     * Reapplies the [DatabaseConfig] if we have one.
     *
     * @param databaseConfig sets a new [DatabaseConfig] on this class.
     */
    public void reopen(DatabaseConfig databaseConfig) {
        if(databaseConfig == null){
            databaseConfig = this.databaseConfig;
        }
        if (!isResetting) {
            close();
            _openHelper = null;
            isOpened = false;
            applyDatabaseConfig(databaseConfig);
            openHelper().database();
            isResetting = false;
        }
    }

    /**
     * Deletes the underlying database and destroys it.
     */
    public void destroy() {
        if (!isResetting) {
            isResetting = true;
            close();
            openHelper().deleteDB();
            _openHelper = null;
            isOpened = false;
            isResetting = false;
        }
    }

    /**
     * Closes the DB and stops the [BaseTransactionManager]
     */
    public void close() {
        transactionManager.stopQueue();
        if (isOpened) {
            try {
                if(closeLock != null) {
                    closeLock.lock();
                }
                openHelper().closeDB();
                isOpened = false;
            } finally {
                if(closeLock != null) {
                    closeLock.unlock();
                }
            }
        }
    }

    /**
     * Saves the database as a backup on the [DefaultTransactionQueue]. This will
     * create a THIRD database to use as a backup to the backup in case somehow the overwrite fails.
     *
     * @throws java.lang.IllegalStateException if [Database.backupEnabled]
     * or [Database.consistencyCheckEnabled] is not enabled.
     */
    public void backupDatabase() {
        openHelper().backupDB();
    }

    @Override
    public int getVersion() {
        return getWritableDatabase().getVersion();
    }

    @Override
    public void execSQL(String query){
        getWritableDatabase().execSQL(query);
    }

    @Override
    public void beginTransaction() {
        tableObserver().syncTriggers(getWritableDatabase());
        getWritableDatabase().beginTransaction();
    }

    @Override
    public void setTransactionSuccessful() {
        getWritableDatabase().setTransactionSuccessful();
    }

    @Override
    public void endTransaction() {
        getWritableDatabase().endTransaction();
        if (!isInTransaction()) {
            tableObserver().enqueueTableUpdateCheck();
        }
    }

    @Override
    public DatabaseStatement compileStatement(String rawQuery){
        return getWritableDatabase().compileStatement(rawQuery);
    }

    @Override
    public FlowCursor rawQuery(String query, String[] selectionArgs){
        return getWritableDatabase().rawQuery(query, selectionArgs);
    }

    @Override
    public int delete(String tableName, String whereClause, String[] whereArgs){
        return getWritableDatabase().delete(tableName, whereClause, whereArgs);
    }

    @Override
    public FlowCursor query(String tableName, String[] columns, String selection, String[] selectionArgs,
                            String groupBy, String having, String orderBy){
        return getWritableDatabase().query(tableName, columns, selection, selectionArgs, groupBy, having, orderBy);
    }

    public boolean isInTransaction(){
        return getWritableDatabase().isInTransaction();
    }

    private final DatabaseCallback internalCallback = new DatabaseCallback() {

        @Override
        public void onOpen(DatabaseWrapper database) {
            tableObserver().construct(database);
            if(callback != null) {
                callback.onOpen(database);
            }
        }

        @Override
        public void onCreate(DatabaseWrapper database) {
            if(callback != null) {
                callback.onCreate(database);
            }
        }

        @Override
        public void onUpgrade(DatabaseWrapper database, int oldVersion, int newVersion) {
            if(callback != null) {
                callback.onUpgrade(database, oldVersion, newVersion);
            }
        }

        @Override
        public void onDowngrade(DatabaseWrapper databaseWrapper, int oldVersion, int newVersion) {
            if(callback != null) {
                callback.onDowngrade(databaseWrapper, oldVersion, newVersion);
            }
        }

        @Override
        public void onConfigure(DatabaseWrapper db) {
            if(callback != null) {
                callback.onConfigure(db);
            }
        }
    };
}
